﻿// Scanner.cs
//
// Copyright 2008-2011 by Burt Harris
// Licensed under the Microsoft Public License (Ms-PL)
// See: http://www.codeplex.com/yaml/license
//

namespace YaTools.Yaml
{
    using System;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Diagnostics.Contracts;
    using System.IO;
    using System.Text;
    using Microsoft.Scripting;

    /// <summary>
    /// Baseline scanner class, might be usful for languages other than YAML
    /// </summary>
    /// 
    [CLSCompliant(false)]
    [ContractVerification(false)]
    public abstract class Scanner : Utility, IDisposable
    {
        private static readonly char[] _emptyBuffer = new char[0];
        private readonly StringCache _stringCache = new StringCache();
        private char[] _buffer = _emptyBuffer;
        private int _bufferPositionInStream;
        private int _bufferSize = 8192;
        private bool _eof;
        private ErrorSink _errorSink = new DefaultErrorSink();
        private int _filledCount;
        private int _indexInBuffer;
        private SourceLocation _lineStart;
        private int _lookaheadSize = 1024;
        private string _path = "<unknown>";
        private TextReader _reader = TextReader.Null;

        private SourceLocation _tokenEnd;
        private SourceLocation _tokenStart;
        private string _tokenText;

        protected Scanner()
        {
            Reset();
        }

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(_buffer != null);
            Contract.Invariant(_indexInBuffer <= _buffer.Length);
            Contract.Invariant(_errorSink != null);
            Contract.Invariant(_reader != null);
            Contract.Invariant(_filledCount >= 0);
        }


        public SourceLocation TokenStart
        {
            get { return _tokenStart; }
        }

        public SourceLocation TokenEnd
        {
            get { return _tokenEnd; }
        }

        public SourceLocation LineStart
        {
            get { return _lineStart; }
        }

        public SourceLocation CurrentLocation
        {
            get
            {
                return new SourceLocation(
                    _bufferPositionInStream + _indexInBuffer,
                    _lineStart.Line,
                    _bufferPositionInStream + _indexInBuffer - _lineStart.Index + 1);
            }
        }

        public SourceSpan TokenSpan
        {
            get { return new SourceSpan(TokenStart, TokenEnd); }
        }

        /// <summary>
        /// Current offset within the scanning buffer
        /// </summary>
        protected int Offset
        {
            [DebuggerStepThrough]
            get { return _indexInBuffer + _bufferPositionInStream; }
        }


        /// <summary>
        /// Reports zero-based column number of current character
        /// </summary>
        /// <remarks>
        /// Tabs will only be counted as one column, but we shouldn't have any.
        /// </remarks>
        protected short CurrentIndent
        {
            [DebuggerStepThrough]
            get { return (short) (TokenStart.Column - 1); }
        }

        public ErrorSink ErrorSink
        {
            get { return _errorSink; }
            set
            {
                Contract.Requires(value != null);
                _errorSink = value;
            }
        }

        public string Path
        {
            get { return _path; }
        }

        /// <summary>
        /// True if current position is at end of line
        /// </summary>
        protected bool AtEndOfLine
        {
            get { return IsBreak(Peek()); }
        }

        /// <summary>
        /// Previous character value, or NUL if at beginning of buffer
        /// </summary>
        protected int PreviousChar
        {
            get
            {
                if (_indexInBuffer == 0) return Unicode.EOS;

                return _buffer[_indexInBuffer - 1];
            }
        }

        /// <summary>
        /// Returns string representing the current token.
        /// </summary>
        /// <remarks>
        /// If Cooking was enabled, this will be the cooked token text, otherwise the 
        /// main part of the latest raw token.
        /// </remarks>
        internal BufferSegment TokenBufferSegment
        {
            get
            {
                Contract.Assume((this.TokenEnd.Index - this.TokenStart.Index) >= 0);
                int start = TokenStart.Index - _bufferPositionInStream;
                int len = TokenEnd.Index - TokenStart.Index;
                return new BufferSegment(_buffer, start, len);
            }
        }

        public string TokenText
        {
            get
            {
                if (_tokenText == null)
                {
                    _tokenText = _stringCache.GetString(this.TokenBufferSegment);
                }
                return _tokenText;
            }
            set { _tokenText = value; }
        }

        protected int TokenLength
        {
            get { return _tokenEnd.Index - _tokenStart.Index; }
        }

        [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Lookahead")]
        protected internal string DebugLookahead
        {
            get
            {
                string s = PeekString(64);
                return VisibleSpacing(s);
            }
        }

        internal string DebugToken
        {
            get
            {
                Contract.Assume((this.CurrentLocation.Index - this.TokenStart.Index) >= 0);

                int start = TokenStart.Index - _bufferPositionInStream;
                int length = CurrentLocation.Index - TokenStart.Index;
                return VisibleSpacing(new string(_buffer, start, length));
            }
        }

        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion

        private static bool ContainsBogusCharacters(string text)
        {
            Contract.Requires(text != null);
            for (int i = 0; i < text.Length; i++)
                if (IsBogusCharacter(text[i]))
                    return true;
            return false;
        }

        public void SetSource(string text)
        {
            Contract.Requires(text != null);
            Contract.Assume(!ContainsBogusCharacters(text));
            _buffer = text.ToCharArray();
            _filledCount = _buffer.Length;
            _indexInBuffer = 0;
            _lineStart = SourceLocation.MinValue;
            _reader = TextReader.Null;
            _eof = true;
        }

        public void SetSource(TextReader reader)
        {
            Contract.Requires(reader != null);
            _buffer = _emptyBuffer;
            _filledCount = 0;
            _lineStart = SourceLocation.MinValue;
            _reader = reader;
            _eof = false;
        }


        [Pure]
        private static bool IsBogusCharacter(char ch)
        {
            switch ((int) ch)
            {
                case 0x0:
                case 0x1:
                case 0x2:
                case 0x3:
                case 0x4:
                case 0x5:
                case 0x6:
                case 0x7:
                case 0x8:
                case 0xb:
                case 0xc:
                case 0xe:
                case 0xf:
                case 0x10:
                case 0x11:
                case 0x12:
                case 0x13:
                case 0x14:
                case 0x15:
                case 0x16:
                case 0x17:
                case 0x18:
                case 0x19:
                case 0x1a:
                case 0x1b:
                case 0x1c:
                case 0x1d:
                case 0x1e:
                case 0x1f:
                case 0xffff:
                    return true;
            }
            return false;
        }

        [SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes")]
        private void AdvanceBuffer(int distance)
        {
            Contract.Requires(distance >= 0);
            Contract.Requires(_filledCount - distance >= 0);
            if (distance == 0) return;

            char test = _buffer[_indexInBuffer - 1];

            // Discard the first distance chars in the buffer

            Array.Copy(_buffer, distance, _buffer, 0, _filledCount - distance);

            // Do the bookkeeping

            _filledCount -= distance;
            _indexInBuffer -= distance;
            _bufferPositionInStream += distance;

            if (test != _buffer[_indexInBuffer - 1])
                throw new Exception("Internal error in Scanner");


            // Pad with nulls
            Array.Clear(_buffer, _filledCount, _buffer.Length - _filledCount);
        }

        private void GrowBuffer(int size)
        {
            if (_buffer.Length < size)
            {
                Array.Resize(ref _buffer, size);
            }
        }

        private void ReadSource()
        {
            int length = _buffer.Length - _filledCount;
            Contract.Assume(length >= 0);

            int read = _reader.Read(_buffer, _filledCount, length);
            if (read == 0)
            {
                _eof = true;
                _reader.Dispose();
                _reader = TextReader.Null;
            }
            _filledCount += read;
        }

        private char ReadMore(int offset)
        {
            Contract.Requires(offset > 0);
            Contract.Requires(offset <= _lookaheadSize, "offset too large for lookahead");
            if (_reader == TextReader.Null)
            {
                // No more characters available
                return Unicode.EOS;
            }

            ReadMore();
            return _buffer[_indexInBuffer + offset];
        }

        private void ReadMore()
        {
            if (!_eof)
            {
                GrowBuffer(_lookaheadSize + _bufferSize);
                AdvanceBuffer(Math.Max(0, _indexInBuffer - _lookaheadSize));
                ReadSource();
            }
        }

        [Pure]
        protected static bool IsBreak(char ch)
        {
            return (ch == Unicode.CR || ch == Unicode.LF || ch == Unicode.EOS);
        }

        [Pure]
        protected char Peek()
        {
            return _indexInBuffer < _filledCount ? _buffer[_indexInBuffer] : Unicode.EOS;
        }

        protected char Peek(int offset)
        {
            int index = _indexInBuffer + offset;

            if (index < _buffer.Length)
                return _buffer[index];
            if (index > 0)
                return ReadMore(offset);
            return Unicode.EOS;
        }

        /// <summary>
        /// Adds an error about the curren token
        /// </summary>
        /// <param name="id">Error message ID.   This is a resource name.</param>
        /// <param name="args">Optional arguments to format error message.</param>
        internal void AddError(ErrorId id, params object[] args)
        {
            EndToken();
            AddError(id, TokenSpan, Severity.Error, args);
        }

        internal void AddError(Severity severity, ErrorId id, params object[] args)
        {
            EndToken();
            AddError(id, TokenSpan, severity, args);
        }


        /// <summary>
        /// Adds an error about an arbitrary SourceSpan 
        /// </summary>
        /// <param name="span">Range of text to associate message with</param>
        /// <param name="id">Error message ID.   This is a resource name.</param>
        /// <param name="severity">Severity of condition</param>
        /// <param name="args">Optional arguments to format error message.</param>
        internal void AddError(ErrorId id, SourceSpan span, Severity severity, params object[] args)
        {
            Contract.Assume(args != null);

            int errorNumber = id.ErrorNumber;
            string message = id.GetMessage(args);

            _errorSink.Add(message, Path, id.ToString(), "", span, errorNumber, severity);
        }

        private void Advance()
        {
            if (++_indexInBuffer >= _filledCount)
                ReadMore();
        }

        protected char Read()
        {
            char ch = Peek();
            if (!IsBreak(ch))
                Advance();
            return ch;
        }


        protected bool Read(char ch)
        {
            if (Peek() == ch)
            {
                Read();
                return true;
            }
            return false;
        }

        protected bool Read(string expected)
        {
            Contract.Requires(expected != null);
            using (PositionTransaction x = SaveOffset())
            {
                for (int i = 0; i < expected.Length; i++)
                    if (Read() != expected[i])
                        return false;
                return x.Commit();
            }
        }

        /// <summary>
        /// Advances over characters till just before a line break or EOS
        /// </summary>
        /// <returns>Character that stopped reading</returns>
        protected int ReadLine()
        {
            while (!IsBreak(Peek())) Read();
            return Peek();
        }

        /// <summary>
        /// Mark the current position as beginning of a Scan operation
        /// </summary>
        protected void BeginScan()
        {
            if (_buffer == _emptyBuffer)
            {
                _buffer = new char[_lookaheadSize];
                ReadSource();
            }
            _tokenText = null;
            _tokenStart = CurrentLocation;
            _tokenEnd = SourceLocation.Invalid;
        }

        private void Back()
        {
            _indexInBuffer--;
        }

        protected int ReadSpacesAndTabs()
        {
            int size = 0;

            while (true)
                switch (Peek())
                {
                    case Unicode.Space:
                    case Unicode.Tab:
                        Read();
                        size++;
                        continue;
                    default:
                        return size;
                }
        }

        protected int BackOverSpacesAndTabs()
        {
            int size = 0;
            while (PreviousChar == Unicode.Space || PreviousChar == Unicode.Tab)
            {
                Back();
                size++;
            }
            return size;
        }

        protected int BackOverSpacesOnly()
        {
            int size = 0;
            while (PreviousChar == Unicode.Space)
            {
                Back();
                size++;
            }
            return size;
        }

        /// <summary>
        /// Mark current position as one character past the significant
        /// portion of the token.   This might for example be the first 
        /// character of inter-token spacing.
        /// </summary>
        protected void EndToken()
        {
            _tokenEnd = CurrentLocation;
        }

        protected void SkipEOL()
        {
            char ch1 = Peek();
            Contract.Requires(ch1 == Unicode.CR || ch1 == Unicode.LF);

            char ch = Peek();
            Advance();
            if (ch == Unicode.CR && Peek() == Unicode.LF)
                Advance();
            SourceLocation old = CurrentLocation;
            _lineStart = new SourceLocation(old.Index, old.Line + 1, 1);
            Debug.Assert(_lineStart == CurrentLocation);
            BeginLine();
        }

        protected void Backup(int count)
        {
            if (count >= CurrentLocation.Column)
                throw new ArgumentOutOfRangeException("count", count, "can't backup over a line break");
            _indexInBuffer -= count;
        }

        protected int SkipSpacesAndTabs()
        {
            int start = Offset;
            while (IsSpaceOrTab(Peek()))
                Read();
            return Offset - start;
        }

        protected int SkipSpaces()
        {
            int start = Offset;
            while (Peek() == Unicode.Space)
                Read();
            return Offset - start;
        }

        protected static bool IsWhiteSpace(char ch)
        {
            switch (ch)
            {
                case Unicode.Space:
                case Unicode.Tab:
                case Unicode.CR:
                case Unicode.LF:
                case Unicode.EOS: // Needed for TokenizerBuffer's behavior past end...
                    return true;
            }
            return false;
        }

        protected void FinishWord()
        {
            while (!IsWhiteSpace(Read()))
            {
            }
            Back();
            EndToken();
        }

        protected static bool IsSpaceOrTab(char ch)
        {
            return ch == Unicode.Space || ch == Unicode.Tab;
        }

        /// <summary>
        /// Skip over characters until one of the ones specified
        /// </summary>
        /// <param name="terminators">Array of characters to stop on</param>
        /// <returns>New offset in buffer</returns>
        protected void AdvanceTo(char[] terminators)
        {
            while (!terminators.Contains(Peek()))
                Read();
        }

        protected void AdvanceOver(char[] allowable, int maxCount)
        {
            char ch;
            int i = 0;
            do
            {
                ch = Read();
                i++;
            } while (allowable.Contains(ch) && i <= maxCount);
            Back();
        }


        protected bool Match(char literal)
        {
            if (Peek() == literal)
            {
                Read();
                return true;
            }
            return false;
        }

        /// <summary>
        /// Visual Studio calls this method with each new source line it wants
        /// to have scanned.   Note that it's not necessarily always called in the
        /// order the lines are called.
        /// </summary>
        /// <param name="source">A line of source code to scan</param>
        protected void FillBuffer(string source)
        {
            Contract.Requires(source != null);
            if (source.IndexOf(Unicode.EOS) != -1)
                throw new ArgumentException("Contains reserved characters", "source");
            FillBuffer(source, SourceLocation.MinValue);
        }

        protected void FillBuffer(string source, SourceLocation location)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            Reset();
            _lineStart = location;
            _buffer = source.ToCharArray();
            _filledCount = source.Length;
        }


        private void Reset()
        {
            _reader.Dispose();
            _reader = TextReader.Null;
            _buffer = _emptyBuffer;
            _eof = true;
            _indexInBuffer = _bufferPositionInStream = _filledCount = 0;
            _lineStart = SourceLocation.MinValue;
            _tokenStart = _tokenEnd = SourceLocation.Invalid;
        }

        protected virtual void OnLineStart()
        {
        }

        private void BeginLine()
        {
            using (SaveOffset())
            {
                OnLineStart();
            }
        }

        protected string PeekString(int length)
        {
            Contract.Requires(length >= 0);
            length = Math.Min(length, _filledCount - _indexInBuffer);
            return new string(_buffer, _indexInBuffer, length);
        }

        protected PositionTransaction SaveOffset()
        {
            return new PositionTransaction(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            _reader.Dispose();
            _reader = TextReader.Null;
        }

        #region Nested type: DefaultErrorSink

        private class DefaultErrorSink : ErrorSink
        {
            public override void Add(string message, string path, string code, string line, SourceSpan span,
                                     int errorCode, Severity severity)
            {
                if ((severity == Severity.FatalError) || (severity == Severity.Error))
                {
                    throw new YamlSyntaxErrorException(message, path, code, line, span, errorCode, severity);
                }
            }
        }

        #endregion

        #region Nested type: PositionTransaction

        /// <summary>Private IDisposable for SaveOffset()</summary>
        protected sealed class PositionTransaction : IDisposable
        {
            private readonly Scanner _scanner;
            private int _position;

            public PositionTransaction(Scanner scanner)
            {
                Contract.Requires(scanner != null);
                Contract.Ensures(_scanner != null);
                _scanner = scanner;
                _position = scanner._indexInBuffer + scanner._bufferPositionInStream;
            }

            #region IDisposable Members

            public void Dispose()
            {
                _scanner._indexInBuffer = _position - _scanner._bufferPositionInStream;
            }

            #endregion

            public bool Commit()
            {
                _position = _scanner._indexInBuffer + _scanner._bufferPositionInStream;
                return true;
            }
        }

        #endregion
    }
}